package com.cloud.rou.im.server;

import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;

import java.util.ArrayList;
import java.util.List;

/**
 * <p></p>
 *
 * @author GaoShuangWen
 * @since 2019-11-15 19:01
 */


public class RetryerBuilder<V> {
    private AttemptTimeLimiter<V> attemptTimeLimiter;
    private StopStrategy stopStrategy;
    private WaitStrategy waitStrategy;
    private BlockStrategy blockStrategy;
    private Predicate<Attempt<V>> rejectionPredicate = Predicates.alwaysFalse();
    private List<RetryListener> listeners = new ArrayList();

    private RetryerBuilder() {
    }

    public static <V> RetryerBuilder<V> newBuilder() {
        return new RetryerBuilder();
    }

    public RetryerBuilder<V> withRetryListener( RetryListener listener) {
        Preconditions.checkNotNull(listener, "listener may not be null");
        this.listeners.add(listener);
        return this;
    }

    public RetryerBuilder<V> withWaitStrategy( WaitStrategy waitStrategy) throws IllegalStateException {
        Preconditions.checkNotNull(waitStrategy, "waitStrategy may not be null");
        Preconditions.checkState(this.waitStrategy == null, "a wait strategy has already been set %s", new Object[]{this.waitStrategy});
        this.waitStrategy = waitStrategy;
        return this;
    }

    public RetryerBuilder<V> withStopStrategy( StopStrategy stopStrategy) throws IllegalStateException {
        Preconditions.checkNotNull(stopStrategy, "stopStrategy may not be null");
        Preconditions.checkState(this.stopStrategy == null, "a stop strategy has already been set %s", new Object[]{this.stopStrategy});
        this.stopStrategy = stopStrategy;
        return this;
    }

    public RetryerBuilder<V> withBlockStrategy( BlockStrategy blockStrategy) throws IllegalStateException {
        Preconditions.checkNotNull(blockStrategy, "blockStrategy may not be null");
        Preconditions.checkState(this.blockStrategy == null, "a block strategy has already been set %s", new Object[]{this.blockStrategy});
        this.blockStrategy = blockStrategy;
        return this;
    }

    public RetryerBuilder<V> withAttemptTimeLimiter( AttemptTimeLimiter<V> attemptTimeLimiter) {
        Preconditions.checkNotNull(attemptTimeLimiter);
        this.attemptTimeLimiter = attemptTimeLimiter;
        return this;
    }

    public RetryerBuilder<V> retryIfException() {
        this.rejectionPredicate = Predicates.or(this.rejectionPredicate, new RetryerBuilder.ExceptionClassPredicate(Exception.class));
        return this;
    }

    public RetryerBuilder<V> retryIfRuntimeException() {
        this.rejectionPredicate = Predicates.or(this.rejectionPredicate, new RetryerBuilder.ExceptionClassPredicate(RuntimeException.class));
        return this;
    }

    public RetryerBuilder<V> retryIfExceptionOfType( Class<? extends Throwable> exceptionClass) {
        Preconditions.checkNotNull(exceptionClass, "exceptionClass may not be null");
        this.rejectionPredicate = Predicates.or(this.rejectionPredicate, new RetryerBuilder.ExceptionClassPredicate(exceptionClass));
        return this;
    }

    public RetryerBuilder<V> retryIfException( Predicate<Throwable> exceptionPredicate) {
        Preconditions.checkNotNull(exceptionPredicate, "exceptionPredicate may not be null");
        this.rejectionPredicate = Predicates.or(this.rejectionPredicate, new RetryerBuilder.ExceptionPredicate(exceptionPredicate));
        return this;
    }

    public RetryerBuilder<V> retryIfResult( Predicate<V> resultPredicate) {
        Preconditions.checkNotNull(resultPredicate, "resultPredicate may not be null");
        this.rejectionPredicate = Predicates.or(this.rejectionPredicate, new RetryerBuilder.ResultPredicate(resultPredicate));
        return this;
    }

    public Retryer<V> build() {
        AttemptTimeLimiter<V> theAttemptTimeLimiter = this.attemptTimeLimiter == null ? AttemptTimeLimiters.noTimeLimit() : this.attemptTimeLimiter;
        StopStrategy theStopStrategy = this.stopStrategy == null ? StopStrategies.neverStop() : this.stopStrategy;
        WaitStrategy theWaitStrategy = this.waitStrategy == null ? WaitStrategies.noWait() : this.waitStrategy;
        BlockStrategy theBlockStrategy = this.blockStrategy == null ? BlockStrategies.threadSleepStrategy() : this.blockStrategy;
        return new Retryer(theAttemptTimeLimiter, theStopStrategy, theWaitStrategy, theBlockStrategy, this.rejectionPredicate, this.listeners);
    }

    private static final class ExceptionPredicate<V> implements Predicate<Attempt<V>> {
        private Predicate<Throwable> delegate;

        public ExceptionPredicate(Predicate<Throwable> delegate) {
            this.delegate = delegate;
        }

        public boolean apply(Attempt<V> attempt) {
            return !attempt.hasException() ? false : this.delegate.apply(attempt.getExceptionCause());
        }
    }

    private static final class ResultPredicate<V> implements Predicate<Attempt<V>> {
        private Predicate<V> delegate;

        public ResultPredicate(Predicate<V> delegate) {
            this.delegate = delegate;
        }

        public boolean apply(Attempt<V> attempt) {
            if (!attempt.hasResult()) {
                return false;
            } else {
                V result = attempt.getResult();
                return this.delegate.apply(result);
            }
        }
    }

    private static final class ExceptionClassPredicate<V> implements Predicate<Attempt<V>> {
        private Class<? extends Throwable> exceptionClass;

        public ExceptionClassPredicate(Class<? extends Throwable> exceptionClass) {
            this.exceptionClass = exceptionClass;
        }

        public boolean apply(Attempt<V> attempt) {
            return !attempt.hasException() ? false : this.exceptionClass.isAssignableFrom(attempt.getExceptionCause().getClass());
        }
    }
}
